import collections
import logging
import time


# https://en.wikipedia.org/wiki/Token_bucket
# https://stackoverflow.com/questions/667508/whats-a-good-rate-limiting-algorithm
class RateLimiter(object):
  def __init__(self, rate_cnt, per, base_time=None):
    self.rate_cnt = float(rate_cnt)
    # unit: messages
    self.per = float(per)
    # unit: seconds
    self.allowance = rate_cnt
    # unit: messages
    if base_time is None:
      base_time = time.time()
    self.last_check = base_time
    # floating-point, e.g. usec accuracy. Unit: seconds

  def check(self, current=None):
    rate_discount = 1.0
    if current is None:
      current = time.time()
    time_passed = current - self.last_check
    if time_passed <= 0:
      return False
    self.last_check = current
    self.allowance += time_passed * (self.rate_cnt / self.per)
    if (self.allowance > self.rate_cnt):
      self.allowance = self.rate_cnt
      # throttle
    if (self.allowance < rate_discount):
      return False
    else:
      self.allowance -= rate_discount
      return True


class WindowLimiter(object):
  def __init__(self, window_size_ns, transact_limit, logger=None):
    self._logger = logger or logging.getLogger('WindowLimiter')
    self._window_size_ns = window_size_ns
    self._transact_limit = transact_limit
    self._trans_times = collections.deque()

  def append_tps_queue(self, timestamp):
    self._trans_times.append(timestamp)

  def update_tps_queue(self, timestamp):
    max_time = timestamp - self._window_size_ns
    # assume ascending transaction timestamp
    while self._trans_times and self._trans_times[0] <= max_time:
      self._trans_times.popleft()

  def get_transact_size_cached(self):
    return len(self._trans_times)

  def get_tps_approval_cached(self):
    return len(self._trans_times) < self._transact_limit

  # for test functionality only.
  def _get_tps_approval(self, timestamp):
    self.update_tps_queue(timestamp)
    if self.get_tps_approval_cached():
      self.append_tps_queue(timestamp)
      return True
    else:
      return False
